diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9c8f3ea0..00000000 --- a/LICENSE +++ /dev/null @@ -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. \ No newline at end of file diff --git a/controllers/base.go b/controllers/base.go index cb58f8da..ca116a67 100644 --- a/controllers/base.go +++ b/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 } //显示错误信息页面. diff --git a/controllers/document.go b/controllers/document.go index a485f00a..693e3722 100644 --- a/controllers/document.go +++ b/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) } + c.StopRun() + }else if output == "epub" { + c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub") - 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) + //如果没有开启缓存,则10分钟后删除 + if !bookResult.IsCacheEBook { + defer func(pdfpath string) { + time.Sleep(time.Minute * 10) + os.Remove(filepath.Dir(pdfpath)) + }(eBookResult.EpubPath) } + c.StopRun() + }else if output == "mobi" { + c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub") - 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) - } + //如果没有开启缓存,则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() } diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 00000000..700caf72 --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,497 @@ +//Author:TruthHun +//Email:TruthHun@QQ.COM +//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 := ` + + + + + + ` + 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 := ` + + + + + Cover + + + +
+ + + +
+ + + ` + 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 := ` + + + + + + + + + %v + + %v + + ` + 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 := ` + + + + 目录 + + + +

目    录

+ %v + + ` + 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, ``) + } + } + codes = append(codes, ``) + } + } + next_idx = idx + return +} + +//将toc转成toc.ncx文件 +func (this *Converter) tocToSummary(pid int) (summarys []string) { + summarys = append(summarys, "") + return +} + +//生成navPoint +func (this *Converter) getNavPoint(toc Toc, idx int) (navpoint string, nextidx int) { + navpoint = ` + + + %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 := `%v + %v + %v + %v + %v + %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 + `` + guide = `` + manifest = fmt.Sprintf(``, this.Config.Cover, GetMediaType(filepath.Ext(this.Config.Cover))) + spineArr = append(spineArr, ``) + } + + if _, err := os.Stat(this.BasePath + "/summary.html"); err == nil { + spineArr = append(spineArr, ``) //目录 + + } + + //扫描所有文件 + 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(``, 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(``, id)) + } + } + manifest = manifest + strings.Join(manifestArr, "\n") + spine = strings.Join(spineArr, "\n") + } else { + return err + } + + pkg := ` + + + %v + + + %v + + + %v + + %v + + ` + if len(guide) > 0 { + 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() +} diff --git a/converter/util.go b/converter/util.go new file mode 100644 index 00000000..5105acdb --- /dev/null +++ b/converter/util.go @@ -0,0 +1,47 @@ +//Author:TruthHun +//Email:TruthHun@QQ.COM +//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 +} diff --git a/models/book.go b/models/book.go index fb247d63..3e49a368 100644 --- a/models/book.go +++ b/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", "
", -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() diff --git a/models/book_result.go b/models/book_result.go index 579e3637..1eded718 100644 --- a/models/book_result.go +++ b/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", "
", -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 : "

本文档使用 MinDoc 构建 - _PAGENUM_ -

", + Header : "

_SECTION_

", + 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, " "+ 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 +} diff --git a/models/convert_book_result.go b/models/convert_book_result.go new file mode 100644 index 00000000..6486d2e0 --- /dev/null +++ b/models/convert_book_result.go @@ -0,0 +1,8 @@ +package models + +// 转换结果 +type ConvertBookResult struct { + PDFPath string + EpubPath string + MobiPath string +} diff --git a/models/document.go b/models/document.go index f20767a3..441bbbc6 100644 --- a/models/document.go +++ b/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("
附件
    ") for _, attach := range attach_list { + if strings.HasPrefix(attach.HttpPath,"/"){ + attach.HttpPath = strings.TrimSuffix(beego.AppConfig.DefaultString("baseurl",""),"/") + attach.HttpPath + } li := fmt.Sprintf("
  • %s
  • ", attach.HttpPath, attach.FileName, attach.FileName) content.WriteString(li) diff --git a/routers/router.go b/routers/router.go index 65ea3408..b384df7b 100644 --- a/routers/router.go +++ b/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") diff --git a/static/js/editor.js b/static/js/editor.js index 1f919139..abe0d558 100644 --- a/static/js/editor.js +++ b/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){ diff --git a/static/js/markdown.js b/static/js/markdown.js index d3699a3b..fb2d86d3 100644 --- a/static/js/markdown.js +++ b/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) { - } + /** * 设置编辑器变更状态 diff --git a/static/js/quill.js b/static/js/quill.js index 1e46a3a0..340e3d58 100644 --- a/static/js/quill.js +++ b/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; diff --git a/static/quill/quill.icons.js b/static/quill/quill.icons.js index 9e0d4ae2..47f6b397 100644 --- a/static/quill/quill.icons.js +++ b/static/quill/quill.icons.js @@ -1,7 +1,7 @@ (function () { var icons = Quill.import('ui/icons'); icons.header[3] = '\n' + - ' \n' + + ' \n' + ''; icons.header[4] = '\n' + ' \n' + diff --git a/views/document/default_read.tpl b/views/document/default_read.tpl index 5e6da985..1ee601c3 100644 --- a/views/document/default_read.tpl +++ b/views/document/default_read.tpl @@ -40,31 +40,34 @@
@@ -128,7 +131,7 @@

{{.Title}}

- + {{/**/}}
@@ -198,7 +201,7 @@
- + + + + - + diff --git a/views/document/new_html_edit_template.tpl b/views/document/new_html_edit_template.tpl index 3faa9aaf..a77f2f4c 100644 --- a/views/document/new_html_edit_template.tpl +++ b/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 @@
-
+
- + +
@@ -231,30 +232,13 @@ - - +
-
- - - - -
- -
- -
- -
- - -
-
-
+
@@ -263,26 +247,14 @@
-
-
-
- MinDoc 是一款针对IT团队开发的简单好用的文档管理系统。 - - - MinDoc 的前身是 SmartWiki 文档系统。SmartWiki 是基于 PHP 框架 laravel 开发的一款文档管理系统。因 PHP 的部署对普通用户来说太复杂,所以改用 Golang 开发。可以方便用户部署和实用。 - - 开发缘起是公司IT部门需要一款简单实用的项目接口文档管理和分享的系统。其功能和界面源于 kancloud 。 - - 可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,能够满足大部分中小团队的文档管理需求。 -
f
-

+
+
+
+
+
0 个附件
-
-
0 个附件
-
-
@@ -334,9 +306,7 @@ - - - - @@ -455,6 +374,7 @@ {{/**/}} {{/**/}} + @@ -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"); });